<?php
/**
 * @brief 视图模板类
 * @note 参考自Discuz!的模板引擎
 */
class View {
    /**
     * View实例
     *
     * @var object
     */
    protected static $instance = null;

    /**
     * 替换用的数组
     *
     * @var array
     */
    var $_replaceCode = ['search' => [], 'replace' => []];

    /**
     * 模板文件所在的目录
     *
     * @var string
     */
    private $_templateDir;

    /**
     * 缓存文件所在的目录
     *
     * @var string
     */
    private $_cacheDir;

    /**
     * 模板标记的左分隔符(默认为'{')
     *
     * @var string
     */
    private $_leftDelimiter = '{';

    /**
     * 模板标记的右分隔符(默认为'}')
     *
     * @var string
     */
    private $_rightDelimiter = '}';

    /**
     * 是否开启编译检查(默认为false)
     *
     * @var boolean
     */
    private $_compileCheck;

    /**
     * 模板缓存文件有效时间(单位为秒, 默认为0=永不过期)
     *
     * @var int
     */
    private $_cacheLifetime;

    /**
     * 构造函数
     *
     * @note 初始化模板引擎的配置参数
     *
     * @return void
     */
    private function __construct() {
        $config = Yaf_Registry::get('Config');
        $this->setOption('templateDir', APP_PATH . '/application/views/');
        $this->setOption('cacheDir', APP_PATH . '/runtime/views/');
        $this->_compileCheck = $config->view->compile_check;
        $this->_cacheLifetime = $config->view->cache_lifetime;
    }

    /**
     * 取得View实例
     *
     * @note 单件模式
     *
     * @return object
     */
    public static function getInstance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * 设置模板引擎的某个配置参数
     *
     * @param string $name 配置项
     * @param mixed $value 数据
     *
     * @return void
     */
    public function setOption($name, $value) {
        switch ($name) {
    		case 'templateDir':
    			$templateDir = trim_path($value);
    			if (!is_dir($templateDir)) {
    				throw new Exception("Template directory '$templateDir' was not found");
    			}
                $this->_templateDir = $templateDir;
                break;
            case 'cacheDir':
    			$cacheDir = trim_path($value);
    			mk_dir($cacheDir);
    			$this->_cacheDir = $cacheDir;
                break;
    		case 'leftDelimiter':
    			$this->_leftDelimiter = preg_quote($value);
    			break;
    		case 'rightDelimiter':
    			$this->_rightDelimiter = preg_quote($value);
    			break;
    		case 'compileCheck':
    			$this->_compileCheck = (boolean) $value;
    			break;
    		case 'cacheLifetime':
    			$this->_cacheLifetime = (int) $value;
    			break;
    		default:
    			throw new Exception("Unknown template configuration option '$name'");
    	}
    }

    /**
     * 取得模板文件的绝对路径
     *
     * @param string $tpl 模板文件名
     *
     * @return string
     */
    protected function _getTemplatePath($tpl) {
        return trim_path($this->_templateDir . DIR_SEP . $tpl);
    }

    /**
     * 取得模板缓存文件的绝对路径
     *
     * @param string $tpl 模板文件名
     *
     * @return string
     */
    protected function _getCachePath($tpl) {
        $tplPath = $this->_getTemplatePath($tpl);
        $md5Data = md5_file($tplPath);
        $config = Yaf_Registry::get('Config');
        if ($config->app->get('multi_lang')) {
            $cacheFile = $tpl . '.' . I18n::getLang() . '.php';
        } else {
            $cacheFile = $tpl . '.php';
        }
        return trim_path($this->_cacheDir . DIR_SEP . $md5Data . '.' . $cacheFile);
    }

    /**
     * 检测模板缓存文件是否有效或重新编译
     *
     * @param string $tpl 模板文件名
     * @param string $md5Data MD5校验信息
     * @param int $expire 有效时间(单位为秒)
     *
     * @return void
     */
    public function check($tpl, $md5Data, $expire) {
        if ($this->_compileCheck && md5_file($this->_getTemplatePath($tpl)) != $md5Data) {
            $this->_save($tpl);
        }
        if (0 != $this->_cacheLifetime && (time() - $expire >= $this->_cacheLifetime)) {
            $this->_save($tpl);
        }
    }

    /**
     * 对模板文件进行解析
     *
     * @param string $tpl 模板文件名
     *
     * @return string
     */
    protected function _parse($tpl) {
        $tplPath = $this->_getTemplatePath($tpl);
        if (!is_readable($tplPath)) {
            throw new Exception("Template file '$tplPath' was not found");
        }

        $template = file_get_contents($tplPath);

        $template = preg_replace(
        	"/" . $this->_leftDelimiter . "(.+?)" . $this->_rightDelimiter . "/s",
        	"{\\1}",
        	$template
        );

        $template = preg_replace_callback(
            "/\{i18n\s+(.+?)\}/is", 
            [$this, 'parse_template_callback_i18n_1'], 
            $template
        );

        $template = preg_replace("/\{(\\\$[a-zA-Z0-9_\[\]\'\"\$\.\x7f-\xff]+)\}/s", "<?=\\1?>", $template);

        $varRegexp = "((\\\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)"
            . "(\[[a-zA-Z0-9_\-\.\"\'\[\]\$\x7f-\xff]+\])*)";
        
        $template = preg_replace_callback(
            "/$varRegexp/s", 
            [$this, 'parse_template_callback_addquote_1'], 
            $template
        );

        $template = preg_replace_callback(
            "/\<\?\=\<\?\=$varRegexp\?\>\?\>/s",
            [$this, 'parse_template_callback_addquote_1'],
            $template
        );

        $template = preg_replace(
            "/[\n\r\t]*\{include\s+file=([A-Za-z0-9_\.]+)\}[\n\r\t]*/is",
            "\r\n<?php include_once(View::getInstance()->render('" . "\\1" . "')); ?>\r\n",
            $template
        );

        $template = preg_replace_callback(
            "/[\n\r\t]*\{eval\s+(.+?)\}[\n\r\t]*/is",
            [$this, 'parse_template_callback_evaltags_1'],
            $template
        );

        $template = preg_replace_callback(
            "/[\n\r\t]*\{echo\s+(.+?)\}[\n\r\t]*/is",
            [$this, 'parse_template_callback_stripvtags_echo_1'],
            $template
        );

		$template = preg_replace_callback(
            "/([\n\r\t]*)\{if\s+(.+?)\}([\n\r\t]*)/is", 
            [$this, 'parse_template_callback_stripvtags_if_123'], 
            $template
        );

		$template = preg_replace_callback(
            "/([\n\r\t]*)\{elseif\s+(.+?)\}([\n\r\t]*)/is", 
            [$this, 'parse_template_callback_stripvtags_elseif_123'], 
            $template
        );

		$template = preg_replace("/\{else\}/i", "<?php } else { ?>", $template);

		$template = preg_replace("/\{\/if\}/i", "<?php } ?>", $template);

		$template = preg_replace_callback(
            "/[\n\r\t]*\{foreach\s+(\S+)\s+(\S+)\}[\n\r\t]*/is", 
            [$this, 'parse_template_callback_stripvtags_foreach_12'], 
            $template
        );

		$template = preg_replace_callback(
            "/[\n\r\t]*\{foreach\s+(\S+)\s+(\S+)\s+(\S+)\}[\n\r\t]*/is", 
            [$this, 'parse_template_callback_stripvtags_foreach_123'], 
            $template
        );

		$template = preg_replace("/\{\/foreach\}/i", "<?php } ?>", $template);

        $template = preg_replace(
            "/\{([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\}/s",
            "<?php echo \\1; ?>",
            $template
        );

        $template = preg_replace("/ \?\>[\n\r]*\<\?php /s", " ", $template);

		$template = preg_replace_callback(
            "/\"(http)?[\w\.\/:]+\?[^\"]+?&[^\"]+?\"/", 
            [$this, 'parse_template_callback_transamp_0'], 
            $template
        );

		$template = preg_replace_callback(
            "/\<script[^\>]*?src=\"(.+?)\"(.*?)\>\s*\<\/script\>/is", 
            [$this, 'parse_template_callback_stripscriptamp_12'], 
            $template
        );

        $template = preg_replace(
            "/\<\?\=(\\\$[a-zA-Z0-9_\[\]\'\"\$\.\x7f-\xff]+)\?\>/s",
            "<?php echo \\1; ?>",
            $template
        );

        $md5Data = md5_file($tplPath);
        $expire = time();
        $template = "<?php View::getInstance()->check('$tpl', '$md5Data', $expire); ?>\n$template";

        return $template;
    }

    /**
     * 检查模板缓存文件是否已存在
     *
     * @param string $tpl 模板文件名
     *
     * @return boolean
     */
    public function isCached($tpl) {
        return is_file($this->_getCachePath($tpl));
    }

    /**
     * 确保模板缓存文件存在并返回该文件的路径
     *
     * @param string $tpl 模板文件名
     *
     * @return string
     */
    public function render($tpl) {
        $cachePath = $this->_getCachePath($tpl);
        if (!$this->isCached($tpl)) {
            $this->_save($tpl);
        }
        return $cachePath;
    }

    /**
     * 对模板文件进行缓存
     *
     * @param string $tpl 模板文件名
     *
     * @return void
     */
    protected function _save($tpl) {
        $this->remove($tpl);
    	$template = $this->_parse($tpl);
        $cachePath = $this->_getCachePath($tpl);
        file_put_contents($cachePath, $template);
    }

    /**
     * 删除模板缓存文件
     *
     * @param string $tpl 模板文件名
     *
     * @return void
     */
    public function remove($tpl) {
        $filesnames = scandir($this->_cacheDir);
        foreach ($filesnames as $name) {
            if ((strlen($name) > 32) && (strpos(substr($name, 32), '.' . $tpl) !== false)) {
                unlink(trim_path($this->_cacheDir . DIR_SEP . $name));
            }
        }
    }

	function parse_template_callback_i18n_1($matches) {
		return i18n($matches[1]);
	}
    
	function parse_template_callback_addquote_1($matches) {
		return $this->addquote('<?='.$matches[1].'?>');
	}

	function parse_template_callback_evaltags_1($matches) {
		return $this->evaltags($matches[1]);
	}

	function parse_template_callback_stripvtags_echo_1($matches) {
		return $this->stripvtags('<?php echo '.$matches[1].'; ?>');
	}

	function parse_template_callback_stripvtags_if_123($matches) {
		return $this->stripvtags($matches[1].'<?php if('.$matches[2].') { ?>'.$matches[3]);
	}

	function parse_template_callback_stripvtags_elseif_123($matches) {
		return $this->stripvtags($matches[1].'<?php } elseif('.$matches[2].') { ?>'.$matches[3]);
	}

	function parse_template_callback_stripvtags_foreach_12($matches) {
		return $this->stripvtags('<?php if(is_array('.$matches[1].')) foreach('.$matches[1].' as '.$matches[2].') { ?>');
	}

	function parse_template_callback_stripvtags_foreach_123($matches) {
		return $this->stripvtags('<?php if(is_array('.$matches[1].')) foreach('.$matches[1].' as '.$matches[2].' => '.$matches[3].') { ?>');
	}

	function parse_template_callback_transamp_0($matches) {
		return $this->transamp($matches[0]);
	}

	function parse_template_callback_stripscriptamp_12($matches) {
		return $this->stripscriptamp($matches[1], $matches[2]);
	}

	function addquote($var) {
		return str_replace("\\\"", "\"", preg_replace("/\[([a-zA-Z0-9_\-\.\x7f-\xff]+)\]/s", "['\\1']", $var));
	}

	function evaltags($php) {
		$i = count($this->_replaceCode['search']);
		$this->_replaceCode['search'][$i] = $search = "<!--EVAL_TAG_$i-->";
		$this->_replaceCode['replace'][$i] = "<?php $php?>";
		return $search;
	}

	function stripvtags($expr, $statement = '') {
		$expr = str_replace('\\\"', '\"', preg_replace("/\<\?\=(\\\$.+?)\?\>/s", "\\1", $expr));
		$statement = str_replace('\\\"', '\"', $statement);
		return $expr.$statement;
	}

	function transamp($str) {
		$str = str_replace('&', '&amp;', $str);
		$str = str_replace('&amp;amp;', '&amp;', $str);
		return $str;
	}

	function stripscriptamp($s, $extra) {
		$s = str_replace('&amp;', '&', $s);
		return "<script src=\"$s\" type=\"text/javascript\"$extra></script>";
	}

	function stripblock($var, $s) {
		$s = preg_replace("/<\?=\\\$(.+?)\?>/", "{\$\\1}", $s);
		preg_match_all("/<\?=(.+?)\?>/", $s, $constary);
		$constadd = '';
		$constary[1] = array_unique($constary[1]);
		foreach($constary[1] as $const) {
			$constadd .= '$__'.$const.' = '.$const.';';
		}
		$s = preg_replace("/<\?=(.+?)\?>/", "{\$__\\1}", $s);
		$s = str_replace('?>', "\n\$$var .= <<<EOF\n", $s);
		$s = str_replace('<?', "\nEOF;\n", $s);
		$s = str_replace("\nphp ", "\n", $s);
		return "<?php\n$constadd\$$var = <<<EOF\n".$s."\nEOF;\n?>";
	}
}
